//
//  NetClient.m
//  CommandCenter08
//
//  Created by Matt Roman on 7/13/08.
//  Copyright 2008 University of Oklahoma. All rights reserved.
//

#import "NetClient.h"
#import <arpa/inet.h>
#import <sys/socket.h>
#import <sys/time.h>
#import <sys/ioctl.h>
#import <fcntl.h>
#import <netdb.h>
#import <unistd.h>

static void callbackRoutine(CFSocketRef inSockRef, CFSocketCallBackType inType, CFDataRef inAddress, const void* inData, void* inContext);

@interface NetClient (Private)
- (void)unscheduleTimeoutTimer;
- (void)socketWriteData;
- (void)socketReadData;
@end

@implementation NetClient

- (id)init
{
	self = [super init];
	socketRef = NULL;
	nSocketRunLoopSource = NULL;
	nDelegate = nil;
	nDataOut = nil;
	nDataIn = [[NSMutableData alloc] init];
	return self;
}

- (void)dealloc
{
	nDelegate = nil;

	// eliminate the timeout timer
	[self unscheduleTimeoutTimer];
	
	// Unschedule socket from runloop
	[self unscheduleFromRunLoop];
	
	[self close];
	
	[nDataIn release];
	nDataIn = nil;
	
	[super dealloc];
}

+ (NetClient*)UDPClientSocket
{
	NetClient*	netsocket;
	BOOL		success = NO;
	
	netsocket = [[[NetClient alloc] init] autorelease];
	
	if([netsocket openSocket])
		if([netsocket scheduleOnCurrentRunLoop]) success = YES;
	return (success ? netsocket : nil);
}

- (void)setDelegate:(id)inDelegate
{
	nDelegate = inDelegate;
}

- (BOOL)openSocket
{
	CFSocketContext		socketContext;
	CFOptionFlags		socketCallbacks;
	CFOptionFlags		socketOptions;
	int					socketFlags;
	
	if(socketRef == NULL){
		int nativeSocket = socket(AF_INET, SOCK_DGRAM, 0);
		if(nativeSocket < 0) return NO;
		
		bzero(&socketContext, sizeof(socketContext));
		socketContext.info = self;
		socketCallbacks = kCFSocketReadCallBack + kCFSocketWriteCallBack;
		socketFlags = fcntl(nativeSocket, F_GETFL, 0);
		if(socketFlags >= 0){
			if(fcntl(nativeSocket, F_SETFL, socketFlags | O_NONBLOCK) >= 0)
			{
				socketRef = CFSocketCreateWithNative(kCFAllocatorDefault, nativeSocket, socketCallbacks, &callbackRoutine, &socketContext);
			}
		}
		
		if(!socketRef) close(nativeSocket);
		else {
			socketOptions = kCFSocketAutomaticallyReenableReadCallBack;
			CFSocketSetSocketFlags(socketRef, socketOptions);
			return YES;
		}
	}
	return NO;
}
- (void)close
{	
	// Unschedule from runloop
	[self unscheduleFromRunLoop];
	
	if(socketRef != NULL){
		close(CFSocketGetNative(socketRef));
		CFSocketInvalidate(socketRef);
		CFRelease(socketRef);
		socketRef = NULL;
	}
	if(nDataIn) [nDataIn release];
	nDataIn = nil;
	if(nDataOut) [nDataOut release];
	nDataOut = nil;
}

- (BOOL)scheduleOnCurrentRunLoop
{
	return [self scheduleOnRunLoop:[NSRunLoop currentRunLoop]];
}

- (BOOL)scheduleOnRunLoop:(NSRunLoop*)inRunLoop
{
	CFRunLoopRef runloop;
	if((socketRef == NULL) || nSocketRunLoopSource) return NO;
	
	// Remove ourselves from any other runloop we might be apart of
	[self unscheduleFromRunLoop];
	
	runloop = [inRunLoop getCFRunLoop];
	if(!runloop) return NO;
	
	nSocketRunLoopSource = CFSocketCreateRunLoopSource(kCFAllocatorDefault, socketRef, 0);
	if(!nSocketRunLoopSource) return NO;
	
	CFRunLoopAddSource(runloop, nSocketRunLoopSource, kCFRunLoopDefaultMode);
	return YES;
}

- (void)unscheduleFromRunLoop
{
	// If our CFSocketRef has not been created than it probably hasn't been scheduled yet
	if( (socketRef == NULL) || nSocketRunLoopSource == NULL )
		return;
	
	// If the runloop source is not valid, return
	if( !CFRunLoopSourceIsValid( nSocketRunLoopSource ) )
		return;
	
	// Invalidate and release the runloop source
	CFRunLoopSourceInvalidate( nSocketRunLoopSource );
	CFRelease( nSocketRunLoopSource );
	
	// Reset our reference to the runloop source
	nSocketRunLoopSource = NULL;
}

- (BOOL)connectToHost:(NSString*)inHostname port:(UInt16)inPort timeout:(NSTimeInterval)inTimeout
{
	struct hostent*		socketHost;
	
	if(socketRef == NULL) return NO;
	socketHost = gethostbyname([inHostname UTF8String]);
	if(!socketHost) return NO;
	
	bzero(&socketAddress, sizeof(socketHost));
	bcopy((char*)socketHost->h_addr, (char*)&socketAddress.sin_addr, socketHost->h_length);
	// or inet_pton(AF_INET, "127.0.0.1", &socketAddress.sin_addr);
	socketAddress.sin_family = AF_INET;
	socketAddress.sin_port = htons(inPort);
	socketAddressLen = sizeof(socketAddress);
	
	if(inTimeout <= 0.5) inTimeout = 1;
		socketTimer = [[NSTimer scheduledTimerWithTimeInterval:inTimeout
														target:self
													  selector:@selector(noServer)
													  userInfo:nil
													   repeats:NO] retain];
		[[NSRunLoop currentRunLoop] addTimer:socketTimer forMode:NSEventTrackingRunLoopMode];
	
	[nDataIn setLength:0];
	return YES;
}

- (NSString*)localHost
{
	if(getsockname(CFSocketGetNative(socketRef), (struct sockaddr*)&socketAddress, &socketAddressLen) < 0)
		return nil;
	return [NSString stringWithCString:inet_ntoa(socketAddress.sin_addr)];
}

///////////////////////////////////////////////////////////////////////////////////////////
// sending data 
- (void)sendData:(NSData*)inData
{	
	if(socketRef == NULL) return;
	// Create outgoing buffer if needed
	if(!nDataOut) nDataOut = [[NSMutableData alloc] initWithCapacity:[inData length]];
	// Append specified bytes to the outgoing buffer
	[nDataOut appendBytes:[inData bytes] length:[inData length]];
	// Attempt to write data to the socket
	[self socketWriteData];
}
- (void)sendString:(NSString*)inString encoding:(NSStringEncoding)inEncoding
{
	[self sendData:[inString dataUsingEncoding:inEncoding]];
}
- (void)socketWriteData
{	
	int amountSent;
	amountSent = sendto(CFSocketGetNative(socketRef), [nDataOut bytes], [nDataOut length], 0, (struct sockaddr*)&socketAddress, socketAddressLen);
	if(amountSent == [nDataOut length]){
		CFSocketDisableCallBacks(socketRef, kCFSocketWriteCallBack);
	}
	else if(amountSent >= 0){
		CFSocketEnableCallBacks(socketRef, kCFSocketWriteCallBack);
	}
	else{
		CFSocketDisableCallBacks(socketRef, kCFSocketWriteCallBack);
		NSLog(@"sending data error");
	}
	[nDataOut replaceBytesInRange:NSMakeRange(0, amountSent) withBytes:NULL length:0];
	if([nDataOut length] == 0)
		if([nDelegate respondsToSelector:@selector( netDataSent: )])
			[nDelegate netDataSent:self];
}
//////////////////////////////////////////////////////////////////////////////////////////
// reading data
- (NSData*)readData
{
	NSData* dataRead;
	
	if([nDataIn length] == 0) return nil;
	
	dataRead = [NSData dataWithData:nDataIn];
	if(!dataRead) return nil;
	
	// reset incomming data buffer since we have read in all the data
	[nDataIn setLength:0];
	
	return dataRead;
}
- (NSData*)readData:(unsigned)inAmount
{
	NSData* dataRead;
	unsigned amountToRead;
	if([nDataIn length] == 0) return nil;
	
	amountToRead = MIN(inAmount,[nDataIn length]);
	
	dataRead = [NSData dataWithBytes:[nDataIn bytes] length:amountToRead];
	if(!dataRead) return nil;
	[nDataIn replaceBytesInRange:NSMakeRange(0, amountToRead) withBytes:NULL length:0];
	
	return dataRead;
}
- (NSString*)readString:(NSStringEncoding)inEncoding
{
	NSString * stringRead;
	
	if([nDataIn length] == 0) return nil;
	
	stringRead = [[[NSString alloc] initWithData:nDataIn encoding:inEncoding] autorelease];
	if(!stringRead) return nil;
	
	// reset incomming data buffer since we have read in all the data
	[nDataIn setLength:0];
	
	return stringRead;
}
- (void)socketReadData
{
	unsigned oldBufferLength;
	int		amountAvaliable,amountRead;
	void*	readBuffer;
	
	if(socketTimer) [self unscheduleTimeoutTimer];
	
	oldBufferLength = [nDataIn length];
	// determine how many bytes are avaliable on the socket
	ioctl(CFSocketGetNative(socketRef), FIONREAD, &amountAvaliable);
	
	readBuffer = malloc((amountAvaliable == 0)?1:amountAvaliable);
	
	if(!readBuffer) return;
	// attempt to read in data
	amountRead = recvfrom(CFSocketGetNative(socketRef), readBuffer, (amountAvaliable == 0)?1:amountAvaliable, 0, (struct sockaddr*)&socketAddress, &socketAddressLen);
	if(amountRead > 0) [nDataIn appendBytes:readBuffer length:amountRead];
	else{
		if([nDelegate respondsToSelector:@selector(netServerDisconnected:)])
			[nDelegate netServerDisconnected:self];
	}
	
	free(readBuffer);
	
	if([nDataIn length] <= oldBufferLength) return;
	if([nDelegate respondsToSelector:@selector(netDataAvailable:amount:)])
		[nDelegate netDataAvailable:self amount:[nDataIn length]];
}
- (void)flushData
{
	[nDataIn setLength:0];
}

- (void)flushDataBytes:(unsigned)bytes
{
	unsigned amountToFlush;
	amountToFlush = MIN(bytes,[nDataIn length]);
	[nDataIn replaceBytesInRange:NSMakeRange(0, amountToFlush) withBytes:NULL length:0];
}

- (void)noServer
{
	[self unscheduleTimeoutTimer];
	if([nDelegate respondsToSelector:@selector(netServerDisconnected:)])
		[nDelegate netServerDisconnected:self];
}
- (void)unscheduleTimeoutTimer
{
	if(socketTimer){
		[socketTimer invalidate];
		[socketTimer release];
		socketTimer = nil;
	}
}
@end

void callbackRoutine(CFSocketRef inSockRef, CFSocketCallBackType inType, CFDataRef inAddress, const void* inData, void* inContext)
{
	NetClient* netsocket;
	
	netsocket = (NetClient*)inContext;
	if(!netsocket) return;
	switch (inType) {
		case kCFSocketReadCallBack:
			[netsocket socketReadData];
			break;
		case kCFSocketWriteCallBack:
			[netsocket socketWriteData];
			break;
		default:
			break;
	}
}